﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using System.Xml.Linq;
using Core.Logger;

namespace Core
{
    public class Post
    {
        private readonly PostOption _postOption = new PostOption();
        public List<PostItem> PostItems { get; set; }
        public List<PostItem> PageItems { get; set; }
        public List<PostImage> PostImages { get; set; }
        public Category PostCategory { get; set; }
        private int _fileNamePref;

        public Post()
        {
            this.PostItems = new List<PostItem>();
			this.PageItems = new List<PostItem>();
            this.PostImages = new List<PostImage>();
            this.PostCategory = new Category();
        }

        public Post(PostOption postOption): this()
        {
            this._postOption = postOption;
        }

        #region Parse
        public void Parse(XElement xml)
        {
            LoggerInstance.Append("Start parsing");

            this.PostCategory.Parse(xml);

            XNamespace wpNs = "http://wordpress.org/export/1.0/";
            XNamespace contentNs = "http://purl.org/rss/1.0/modules/content/";

            IEnumerable<XElement> items = xml.Elements("channel").Elements("item");

            int totalCount = 0;
            int postPublishedCount = 0;
            int pageCount = 0;

            // Iterate through all the items xml elements
            foreach (XElement xElement in items)
            {
                string status = (string) xElement.Element(wpNs + "status");

                PostItem postItem = new PostItem();
                bool include = false;

                // includes draft posts but not auto-draft
                // <wp:status>draft</wp:status>
                if (status.Equals("draft", StringComparison.InvariantCultureIgnoreCase) && _postOption.IncludeDraft)
                {
                    include = true;
                    postItem.IsPublished = false;
                }

                // includes publish posts
                // <wp:status>publish</wp:status>
                if (status.Equals("publish", StringComparison.InvariantCultureIgnoreCase))
                {
                    include = true;
                    postItem.IsPublished = true;
                }

                totalCount++;

                // parse only published posts. Skip inherit and auto-draft
                if (!include)
                {
                    continue;
                }

                // Title
                postItem.Title = (string) xElement.Element("title");
                postItem.Description = (string) xElement.Element("description");

                // Content
                postItem.Content = (string) xElement.Element(contentNs + "encoded");

                // Publish date
                postItem.PubDate = ParseDate(postItem.Title, xElement.Element("pubDate"));

                // Comment status
                postItem.IsCommentsEnabled = ((string) xElement.Element(wpNs + "comment_status")) == "open";

                // Category
                postItem.CategoryWpNiceNameCollection = ParseCategory(postItem.Title, xElement);
                postItem.Tags = ParseTags(xElement);
                postItem.Comments = ParseComments(wpNs, xElement);

                // Post or page
                XElement postType = xElement.Element(wpNs + "post_type");
                if (postType != null && postType.Value.ToLower().Equals("page"))
                {
                    PageItems.Add(postItem);
                    pageCount++;
                    continue;
                }

                PostItems.Add(postItem);
                postPublishedCount++;
            }

            LoggerInstance.Append("Total parsed posts: {0} ", totalCount);
            LoggerInstance.Append("Total published posts: {0} ", postPublishedCount);
            LoggerInstance.Append("Total page published: {0} ", pageCount);
        }

        private static DateTime ParseDate(string postTitle, XElement dateElement)
        {
            DateTime pubDate;
            string pubDateXml = (string) dateElement; 
            DateTime.TryParse(pubDateXml, out pubDate);
            if (pubDate == DateTime.MinValue)
            {
                LoggerInstance.Append("Error parsing PubDate for item: " + postTitle, LogStatus.Error);
            }

            return pubDate;
        }

        private static List<string> ParseCategory(string postTitle, XElement xElement)
        {
            // <category domain="category" nicename="finance"><![CDATA[finance]]></category>
            List<string> categories = new List<string>();
            IEnumerable<XElement> xCategories = xElement.Elements("category");

            foreach (XElement xCategory in xCategories)
            {
                if (xCategory.HasAttributes)
                {
                    XAttribute domain = xCategory.Attribute("domain");
                    if (domain != null && domain.Value.Equals("category", StringComparison.InvariantCultureIgnoreCase))
                    {
                        XAttribute nicename = xCategory.Attribute("nicename");
                        if (nicename != null)
                        {
                            string categName = nicename.Value;
                            categories.Add(categName);
                        }
                    }
                }
            }

            if (categories.Count == 0)
            {
                LoggerInstance.Append(LogStatus.Warning, "Category is empty for the post: {0}", postTitle);
            }

            return categories;
        }

        private static List<string> ParseTags(XElement xElement)
        {
            IEnumerable<XElement> categories = xElement.Elements("category");

            List<string> postTags = new List<string>();
            foreach (XElement category in categories)
            {
                foreach (XAttribute attr in category.Attributes())
                {
                    if (attr.Value == "tag")
                    {
                        string[] tags = category.Value.Split(' ');

                        foreach (string tag in tags.Where(tag => !postTags.Contains(tag)))
                        {
                            postTags.Add(tag);
                        }
                    }

                }
            }

            return postTags;
        }

        private List<CommentItem> ParseComments(XNamespace nsWp, XElement xElement)
        {
            List<CommentItem> comments = new List<CommentItem>();

            IEnumerable<XElement> xComments = xElement.Elements(nsWp + "comment");
            
            foreach(XElement comment in xComments)
            {
                CommentItem commentItem = new CommentItem();
                commentItem.Id = Guid.NewGuid();
                commentItem.WpId = (int) comment.Element(nsWp + "comment_id");
                commentItem.Author = (string)comment.Element(nsWp + "comment_author");
                commentItem.Email = (string)comment.Element(nsWp + "comment_author_email");
                commentItem.WebSite = (string) comment.Element(nsWp + "comment_author_url");
                commentItem.Ip = (string) comment.Element(nsWp + "comment_author_IP");
                commentItem.Date = ParseDate("Comment id: " + commentItem.Id, comment.Element(nsWp + "comment_date"));
                commentItem.Content = (string) comment.Element(nsWp + "comment_content");
                commentItem.WpParentId = (int) comment.Element(nsWp + "comment_parent");

                this.SetComentStatus((string)comment.Element(nsWp + "comment_approved"), commentItem);
                comments.Add(commentItem);
            }

            this.SetParentComment(comments);

            return comments;
        }

        private void SetComentStatus(string xmlStatus, CommentItem commentItem)
        {
            switch (xmlStatus.ToLower())
            {
                case "1":
                    commentItem.Approved = true;
                    break;
                case "2":
                    commentItem.Approved = false;
                    break;
                case "trash":
                    commentItem.Deleted = true;
                    break;
                case "spam":
                    commentItem.Spam = true;
                    break;
                case "0":
                    commentItem.Spam = true;
                    break;
                default:
                    throw new ArgumentOutOfRangeException(String.Format("Provided status: {0} is unknown.", xmlStatus));
            }
        }

        private void SetParentComment(List<CommentItem> comments)
        {
            foreach (CommentItem comment in comments)
            {
                if(comment.WpParentId != 0)
                {
                    comment.ParentId = (comments.Find(cmt => comment.WpParentId == cmt.WpId)).Id;
                }
            }
        }

        #endregion

        public void Process()
        {
            if (this.PostItems.Count == 0)
            {
                LoggerInstance.Append(LogStatus.Warning, "No posts to be processed");
            }

            foreach (PostItem postItem in this.PostItems)
            {
                postItem.Id = Guid.NewGuid();
                postItem.CategoryIdCollection = this.GetCategoryIdCollection(postItem.CategoryWpNiceNameCollection);
                postItem.Slug = this.RemoveIllegalCharacters(postItem.Title);
                postItem.Content = this.ReplaceNewLines(postItem.Content);

                this.UpdateImageFileLocation(postItem);
            }

            foreach (PostItem pageItem in this.PageItems)
            {
                pageItem.Id = Guid.NewGuid();
                pageItem.Slug = this.RemoveIllegalCharacters(pageItem.Title);
            }
            
        }

        private string ReplaceNewLines(string content)
        {
            /* The content value which is under the encoded tag (<content:encoded>) contains new lines. 
             * The original HTML file contains <p> tags which were replaced by new lines when exporting. 
             * Because of that the new lines should be replaced with <p> tag. This is done in ReplaceNewLines method:*/
            const string PARAGRAPH = "</p><p>";
            string newSt =
                content.Replace("\r\n", PARAGRAPH).
                Replace("\n", PARAGRAPH).
                Replace("\r", PARAGRAPH).
                Replace("</p><p></p><p>", PARAGRAPH);
            
            string finish = "<p>" + newSt + "</p>";

            return finish;
        }

        /// <summary>
        /// Updates the image file location to point from the Wordpress to BlogEngine host location.
        /// </summary>
        /// <param name="postItem">PostItem with content to be replaced.</param>
        private void UpdateImageFileLocation(PostItem postItem)
        {
            int start = 0;
            string content = postItem.Content;
            const string END_MARKER = "</a>";

            // iterate through all the content and replace the href and img src attributes with PostOptions BlogUrl and RelImageFolder
            while ((start = content.IndexOf("<a", start + 1, StringComparison.InvariantCultureIgnoreCase)) > 0)
            {
                int end = content.IndexOf(END_MARKER, start);
                int length = end - start;
                string href = content.Substring(start, length + END_MARKER.Length);
                
                // replace only content where img tag is under <a href> tag
                if (!href.Contains("<img"))
                {
                    continue;
                }
                
                try
                {
                    // href tag
                    XElement hrefEl = XElement.Parse(href);
                    string hrefAtt = (string) hrefEl.Attribute("href");
                    int lastIndex = hrefAtt.LastIndexOf('/');
                    int fileLength = hrefAtt.Length - lastIndex;
                    string hrefFileName = hrefAtt.Substring(lastIndex + 1, fileLength - 1);
                    hrefFileName = this.GetNewFileName(hrefFileName);

                    // img tag
                    XElement img = hrefEl.Element("img");
                    string src = (string) img.Attribute("src");
                    lastIndex = src.LastIndexOf('/');
                    fileLength = src.Length - lastIndex;
                    string imgFileName = src.Substring(lastIndex + 1, fileLength - 1);
                    imgFileName = this.RemoveParameterFromImageName(imgFileName);
                    imgFileName = this.GetNewFileName(imgFileName);
                    

                    PostImage postImage = new PostImage();
                    postImage.Href = hrefAtt;
                    postImage.ImgSrc = src;
                    postImage.HrefFileName = hrefFileName;
                    postImage.ImgSrcFileName = imgFileName;

                    this.ReplaceImgReferenceInContent(postItem, href, postImage);
                    
                    this.PostImages.Add(postImage);
                }
                catch (Exception ex)
                {
                    LoggerInstance.Append(LogStatus.Error, "Error parsing IMG for post: {0} ---- {1}", postItem.Title,
                                          ex.ToString());
                }
            }
        }

        /// <summary>
        /// Removes the parameter value from the image name. Image can have the appended at the end
        /// a parameter like ImageName?w=300.
        /// </summary>
        /// <param name="imgName">Image name.</param>
        /// <returns>Returns the image name with no parameters</returns>
        private string RemoveParameterFromImageName(string imgName)
        {
            
            if (!imgName.Contains("?")) return imgName;

            int start = imgName.IndexOf("?");
            imgName = imgName.Remove(start);

            return imgName;
        }

        /// <summary>
        /// Change the name of the provided file name by appending an incremented number at the end of the file name.
        /// </summary>
        /// <param name="fileName">File name.</param>
        /// <returns>Returns new file name.</returns>
        private string GetNewFileName(string fileName)
        {
            string nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);

            string newFileName = nameWithoutExtension + "-" + (_fileNamePref++) + (new FileInfo(fileName)).Extension;
            
            return newFileName;
        }

        /// <summary>
        /// Replaces the image references in html content with the reference to the blog engine hosting.
        /// </summary>
        /// <param name="postItem">Post item with content to be replaced.</param>
        /// <param name="replaceValue">Value to be replaced.</param>
        /// <param name="postImage">Post image with href and src values.</param>
        private void ReplaceImgReferenceInContent(PostItem postItem, string replaceValue, PostImage postImage)
        {
            if (String.IsNullOrEmpty(_postOption.BlogUrl))
            {
                throw new ArgumentNullException("The blog url is not specified");
            }

            string blogUrl = RemoveEndForwardSlash(this._postOption.BlogUrl);
            if (blogUrl.Length.Equals(0))
            {
                LoggerInstance.Append(LogStatus.Error, "BlogUrl is empty");    
            }

            string imageFileLocation = RemoveEndForwardSlash(this._postOption.RelImageFolder);

            string newContent =
                String.Format(
                    "<a href=\"{0}/image.axd?picture={1}/{2}\" " +
                    " rel=\"lightbox\"><img style=\"background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px;" +
                    " display: inline; border-top: 0px; border-right: 0px; padding-top: 0px\" border=\"0\" " +
                    " src=\"{3}/image.axd?picture={4}/{5}\" /></a>",
                    blogUrl, imageFileLocation, postImage.HrefFileName,
                    blogUrl, imageFileLocation, postImage.ImgSrcFileName);
            
            string replacing = postItem.Content.Replace(replaceValue, newContent);
            postItem.Content = replacing;
        }

        private string RemoveEndForwardSlash(string value)
        {
            if (value.EndsWith("//"))
            {
                value = value.Remove(value.Length - 1, 1);
            }

            return value;
        }

        private string RemoveIllegalCharacters(string text)
        {
            if (string.IsNullOrEmpty(text))
            {
                return text;
            }

            text = text.Replace(":", string.Empty)
            .Replace("/", string.Empty)
            .Replace("?", string.Empty)
            .Replace("#", string.Empty)
            .Replace("[", string.Empty)
            .Replace("]", string.Empty)
            .Replace("@", string.Empty)
            .Replace("*", string.Empty)
            .Replace(".", string.Empty)
            .Replace(",", string.Empty)
            .Replace("\"", string.Empty)
            .Replace("&", string.Empty)
            .Replace("'", string.Empty)
            .Replace(" ", "-");
            text = RemoveDiacritics(text);
            text = RemoveExtraHyphen(text);

            return HttpUtility.HtmlEncode(text).Replace("%", string.Empty);

        }
        private string RemoveDiacritics(string text)
        {
            var normalized = text.Normalize(NormalizationForm.FormD);
            var sb = new StringBuilder();

            foreach (var c in
                normalized.Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark))
            {
                sb.Append(c);
            }

            return sb.ToString();
        }
        private string RemoveExtraHyphen(string text)
        {
            if (text.Contains("--"))
            {
                text = text.Replace("--", "-");
                return RemoveExtraHyphen(text);
            }

            return text;
        }

        private List<Guid> GetCategoryIdCollection(IEnumerable<string> categoryWpNiceNameCollection)
        {
            return (from categoryNiceName in categoryWpNiceNameCollection
                    from categoryItem in this.PostCategory.CategoryItems
                    where categoryNiceName == categoryItem.WpNiceName
                    select categoryItem.Id).ToList();

        }

    }
}
